package net.homelinux.chaoswg.io.renga.core.gui.operations;

import java.awt.Graphics;
import java.awt.Point;
import java.util.Iterator;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Vector;

import net.homelinux.chaoswg.io.renga.core.gui.DrawingArea;
import net.homelinux.chaoswg.io.renga.core.gui.operations.helper.InputOutputListener;
import net.homelinux.chaoswg.io.renga.core.gui.operations.helper.OperationListener;

/*
 * This class maintains order over a the content of a specific Composite Function 
 */
public class Control extends Observable implements Observer{
    // The list of operations making up the compsite function
    private final List<Operation> ops;

    // The list of objects that want to be informed about addition and removal of Inputs and outputs of this function
    private final List<InputOutputListener> ioListeners = new Vector<InputOutputListener>();

    // The list of objects that want to be informed about the addition and removal of any operation
    private final List<OperationListener> opListeners = new Vector<OperationListener>();

    //The current number of inputs of the composite function
    private int inputCounter = 0;

    //The current number of outputs of the composite function
    private int outputCounter = 0;

    //Constructors nothing funky here
    public Control () {
        ops = new Vector<Operation>();
    }

    public Control (final List<Operation> ops) {
        this.ops = ops;
    }

    //Find the operation at a given position
    private Operation findObjectAt (final Point p) {
        //Iterate over the functions
        for (final Operation op : ops) {
            //The first operation hit containing the point (as maintained by the operation itself) 
            if (op.contains(p)) {
                //is returned 
                return op;
            }
        } 
        //Or null if none contains the point
        return null;
    }

    //Two components should be joined
    public void connect (final Point p0, final Point p1) {
        //We first search at the given positions for components
        final Operation op0 = findObjectAt(p0);
        final Operation op1 = findObjectAt(p1);

        //If we find two components we tell them to connect
        if (op0 != null && op1 != null) {
            op0.connectTo (p0, op1, p1); 
        }
    }

    //To draw the component 
    public void drawYourself (final Graphics g) {
        //We first let all the components draw themselves
        for (int i = ops.size() - 1; i >= 0; --i) {
            final Operation  tmp = ops.get(i);
            //The first component is set selected 
            tmp.setSelected(i == 0);
            tmp.drawYourself(g);
        }
    }

    //To know if we could connect form a specific point
    public boolean connectAllowed (final Point p) {
        //We must first find the component at that point
        final Operation op =  findObjectAt(p);

        /*
         * We could just an operation with another if ther is a  Operation 
         * at the startpoint of the connection attempt (sic) and the
         * connection itself allows it (see there why this could possibly go
         * wrong
         */
        return op != null && op.connectAllowed(p);
    }

    //To move an Operation 
    public void moveObjectAtBy (final Point p, final int dx, final int dy) {
        //We first find the operation at the startpoint of the move
        final Operation op = findObjectAt(p);

        //And if there really is one
        if (op != null) {
            /*
             * We tell it to move (hmm this control has an really easy job)
             * probably because it is just an facade (see Design Pattern for
             * more accurate description) 
             */
            op.moveBy(dx, dy);
        }
        //Yes again something has changed so notify the observers
        setChanged();
        notifyObservers();
    }

    //Set an operation selected as also easy
    private void setOperationSelected (final Operation op, final boolean selected) {
        //First look if we really got something to do (as we don't allow unselection yet (why should we *headscratch*)) 
        //We only got  to work if selected is set
        if (selected) {
            //If so we move the operation 
            //to the front of the operationslist
            //as seen above (hint: drawYourself) this will
            //make it selected

            //So remove it from its current position
            ops.remove(op);
            //And add it to the front
            ops.add (0, op);
            //And set selected (during the next repaint
            //this would be done automaticlly but you can't
            //be to safe
            op.setSelected(selected);

            //And now we tell everybody (interested) that something has changed
            setChanged();
            notifyObservers(this);
        }
    }

    //Ok the selected operation isn't need anymore 
    //at least the user thinks so 
    public void removeSelected () {
        //So we iterate over the operations
        for (final Iterator<Operation> iter = ops.iterator(); iter.hasNext();) {
            //And take a look at the current element
            final Operation tmp = iter.next();
            //if it is selected
            if (tmp.isSelected()) {
                //We tell it to prepare to remove
                //(Might do some cleanup here
                tmp.prepareForRemoval();

                //And remove it
                iter.remove();

                //If it is a input or output we need to clean up afterwards
                //Correct input and output numbers and send events
                cleanupInputsAndOutputs(tmp);
            }
        }
        //If there are more operations we just set the next operation selected
        if (!(ops.isEmpty())) {
            ops.get(0).setSelected(true);
        }

        //And obviously something has changed so tell everybody (this is a common
        //java idiom (*barf*)
        setChanged();
        notifyObservers(this);
    }

    //Ohh finally we got something to do 
    //Simualte the function
    public void doit () {
        //And for set we first reset the operations
        for (Operation op : ops) {
            op.reset();
        }
        //And then start at the end 
        final List<Operation> ends = getOutputs();

        //And tell the endoperations 
        for (Operation op : ends) {
            //That we'd like them to be evaluated
            //Which in turn will ask all operations before
            //Them and so on (but I get ahead of myself
            op.apply(0);
        }
        //And after that something has definitly changed
        setChanged();
        notifyObservers(this);
        setChanged();
        notifyObservers(false);

    }

    //So we definitly need the outputs of our function 
    //See above
    public List<Operation> getOutputs () {
        //So we create a Vector
        final List<Operation> ends = new Vector<Operation>();

        //if is a designated driver errr FunctionOUPUTOperation we 
        //collect it (there are so nice idioms for this in other 
        //programming languages (*sigh*)
        for (final Operation op : ops) {
            if (op instanceof FunctionOutputOperation) {
                ends.add (op);
            }
        }
        //And return it
        return ends;
    }

    //And as with the outputs we might need the inputs to
    public List<Operation> getInputs () {
        //Same as above 
        final List<Operation> starts = new Vector<Operation>();

        //Just this time we collect eveything which is a designated FunctionINPUTOperation
        for (final Operation op : ops) {
            if (op instanceof FunctionInputOperation) {
                starts.add (op);
            }
        }
        //and return it
        return starts;
    }


    //To make this fun we need to be able to add operations
    public void addOperation (final Operation o, final boolean initial) {
        //So we add an operation haded to us 
        //At the begining of the list
        ops.add(0, o);
        //and make it selected
        o.setSelected(true);
//      o.addObserver(this);

        o.addObserver(this);

        if (!initial) {
            o.setVisible(true);
        }

        //If it is an instance of an in or output we fir events 
        //telling the listeners 
        if (o instanceof FunctionInputOperation) {
            final FunctionInputOperation fio = ((FunctionInputOperation)o);
            if (!initial) {
                fio.setIsInputNumber(inputCounter);
            }
            inputCounter++;
            fireInputAdded(fio);
        }
        if (o instanceof FunctionOutputOperation) {
            final FunctionOutputOperation foo = ((FunctionOutputOperation)o);
            if (!initial) {
                foo.setIsOutputNumber(outputCounter);
            }
            outputCounter++;
            fireOutputAdded(foo);
        }
        //And even if not somebody might be interested
        fireOperationAdded(o);

        //And then we tell anybody who might be remotly interested
        //Java idioms again
        setChanged();
        notifyObservers(o);
    }

    //To tell the ones listening that something has been added or removed
    //We iterate over the lists and call the function in the listeners determined
    //for that cause (next 6 functions)
    private void fireInputAdded (final FunctionInputOperation fio) {
        for (final InputOutputListener ioListener : ioListeners) {
            ioListener.inputAdded(fio);
        }
    }

    private void fireOutputAdded (final FunctionOutputOperation foo) {
        for (final InputOutputListener ioListener : ioListeners) {
            ioListener.outputAdded(foo);
        }
    }

    private void fireOperationAdded (final Operation op) {
        for (final OperationListener opListener : opListeners) {
            opListener.operationAdded(op);
        }
    }

    private void fireInputRemoved (final FunctionInputOperation fio) {
        for (final InputOutputListener ioListener : ioListeners) {
            ioListener.inputRemoved(fio);
        }
    }

    private void fireOutputRemoved (final FunctionOutputOperation foo) {
        for (final InputOutputListener ioListener : ioListeners) {
            ioListener.outputRemoved(foo);
        }
    }

    private void fireOperationRemoved (final Operation op) {
        for (final OperationListener opListener : opListeners) {
            opListener.operationRemoved(op);
        }
    }

    //This is just for the symmetry this operation
    //Might never be called (at least not in the 
    //Current renga
    public void removeOperation (final Operation o) {
        o.prepareForRemoval();

        cleanupInputsAndOutputs(o);

        fireOperationRemoved(o);

        setChanged();
        notifyObservers(this);
    }

    //We need to cleanup after removal of inputs and outputs
    private void cleanupInputsAndOutputs (final Operation o) {
        //So we do if it is an input
        if (o instanceof FunctionInputOperation) {
            final FunctionInputOperation fio = ((FunctionInputOperation)o);
            //We check which input it was 
            final int fioIn = fio.getIsInputNumber();
            //And we know that it is exactly one input less know
            inputCounter--;

            //And tell all the inputs with a higher number to move up
            for (final Operation start : getInputs()) {
                if (start instanceof FunctionInputOperation) {
                    //by first finding them 
                    final FunctionInputOperation tmp = ((FunctionInputOperation)start);

                    //Then cheking their current number
                    int tmpIn = tmp.getIsInputNumber();
                    //if they have to move up
                    if (tmpIn > fioIn) {
                        //We make the output number one smaller
                        tmp.setIsInputNumber(tmpIn-1);
                    }
                }
            }
            //And we fire removal events
            fireInputRemoved(fio);
        }


        //Same goes for outputs (if I find a good refactoring for this 
        //these two cases might become one but this is java so 
        //probably not
        if (o instanceof FunctionOutputOperation) {
            final FunctionOutputOperation foo = ((FunctionOutputOperation)o);
            final int fooIn = foo.getIsOutputNumber();

            outputCounter--;

            for (final Operation start : getInputs()) {
                if (start instanceof FunctionOutputOperation) {
                    final FunctionOutputOperation tmp = ((FunctionOutputOperation)start);
                    int tmpIn = tmp.getIsOutputNumber();
                    if (tmpIn > fooIn) {
                        tmp.setIsOutputNumber(tmpIn-1);
                    }
                }
            }
            fireOutputRemoved(foo);
        }
    }

    //If someone asks he gets the list of operations
    public List<Operation> getOperations () {
        return ops;
    }

    public void clicked (final Point p) {
        final Operation op = findObjectAt(p);
        if (op != null) {
            setOperationSelected(op, true);
        }
    }

    public void doubleClicked (final Point p, final DrawingArea dv) {
        final Operation op = findObjectAt(p);

        if (op != null) {
            op.doubleClick(dv);
        }
    }
    //Let us return the first operation we can find if we
    //are asked for the selected
    //we could also go to the list and return the 
    //on that is selected but why the hassel
    //we maintain that it is at the first position
    public Operation getSelected () {
        if (!(ops.isEmpty())) {
            return ops.get(0);
        }
        //and if there isn't even one operation 
        //ok then there is non 
        return null;        
    }

    //we got to give the world the opportunity to join in
    //So we let them add themselfes to the list of 
    //Watchers (next 4 methods)
    public void addInputOutputListener (final InputOutputListener ioListener) {
        ioListeners.add (ioListener);
    }

    public void removeInputOutputListener (final InputOutputListener ioListener) {
        ioListeners.remove (ioListener);
    }

    public void addOperationListener (final OperationListener opListener) {
        opListeners.add (opListener);
    }

    public void removeOperationListener (final OperationListener opListener) {
        opListeners.remove (opListener);
    }

    public boolean fullyConnected () {
        for (final Operation op : ops) {
            if (!op.fullyConnected()) {
                return false;
            }
        }
        return true;
    }

    //We need to know the size of the component so we need
    //Horizontal and vertical size
    public int getHorizontalSize () {
        //We just iterate over the operations finding there maximal y
        //coordinate 
        int max = 0;
        for (final Operation op : ops) {
            final int tmp = op.getMaxY ();
            max = Math.max(tmp, max);
        }
        return max;
    }

    public int getVerticalSize () {
        //Same as above with the y coordinate
        int max = 0;
        for (final Operation op : ops) {
            final int tmp = op.getMaxX ();
            max = Math.max(tmp,max);
        }
        return max;
    }

    public void setAllVisible (final boolean visibility) {
        for (final Operation op : ops) {
            op.setVisible(visibility);
        }
    }

    public void update(Observable arg0, Object arg1) {
        if (arg1 instanceof Boolean) {
            setChanged();
            notifyObservers(arg1);
        }
    }
}
